﻿using System.Reflection;
using System.ComponentModel;
using System;
using System.Reflection.Emit;
using System.Threading;
using System.Linq;
using System.Collections.Generic;

namespace ProxyViewModel.Emitors {
	internal class NotifyPropertyChangeEmitor : EmitorBase {
		// this variable if true indicates that view model already implemented INotifyPropertyChanged
		private bool _notifyPropertyChangedIsAlreadyImplemented;

		public NotifyPropertyChangeEmitor(TypeBuilder builder, Type operatable, IDictionary<string, object> parameters)
			: base(builder, operatable, parameters) { }

		protected override void EmitContent() {
			ImplementINotifyPropertyChanged();
			DeclareEvent();
		}

		private void ImplementINotifyPropertyChanged() {
			if (!Operatable.GetInterfaces().Contains(typeof(INotifyPropertyChanged))) {
				TypeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanged));
			}
			else {
				_notifyPropertyChangedIsAlreadyImplemented = true;
			}
		}

		private void DeclareEvent() {
			if (!_notifyPropertyChangedIsAlreadyImplemented) {
				EventBuilder ev = TypeBuilder.DefineEvent("PropertyChanged", EventAttributes.None, typeof(PropertyChangedEventHandler));
				FieldInfo info = TypeBuilder.DefineField("PropertyChanged", typeof(PropertyChangedEventHandler), FieldAttributes.Private);
				CurrentParameters[Parameters.PropertyChangedFieldKey] = info;
				DefineAddEventMethod(ev, info, "add_PropertyChanged");
				DefineRemoveEventMethod(ev, info, "remove_PropertyChanged");
			}
		}

		private void DefineAddEventMethod(EventBuilder ev, FieldInfo propertyChangedField, string name) {
			MethodBuilder builder = TypeBuilder.DefineMethod(name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.NewSlot | MethodAttributes.HideBySig |
				MethodAttributes.Final, null, new[] { typeof(PropertyChangedEventHandler) });
			builder.SetImplementationFlags(MethodImplAttributes.Managed);
			ILGenerator il = builder.GetILGenerator();
			ev.SetAddOnMethod(builder);

			MethodInfo miRemoveEvent = typeof(INotifyPropertyChanged).GetMethod("add_PropertyChanged");
			TypeBuilder.DefineMethodOverride(builder, miRemoveEvent);

			il.DeclareLocal(typeof(PropertyChangedEventHandler));
			il.DeclareLocal(typeof(PropertyChangedEventHandler));
			il.DeclareLocal(typeof(PropertyChangedEventHandler));
			il.DeclareLocal(typeof(bool));

			Label lb1 = il.DefineLabel();
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldfld, propertyChangedField);
			il.Emit(OpCodes.Stloc_0);
			il.MarkLabel(lb1);
			il.Emit(OpCodes.Ldloc_0);
			il.Emit(OpCodes.Stloc_1);
			il.Emit(OpCodes.Ldloc_1);
			il.Emit(OpCodes.Ldarg_1);
			il.Emit(OpCodes.Call, typeof(Delegate).GetMethod("Combine", new Type[] { typeof(Delegate), typeof(Delegate) }));
			il.Emit(OpCodes.Castclass, typeof(PropertyChangedEventHandler));
			il.Emit(OpCodes.Stloc_2);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldflda, propertyChangedField);
			il.Emit(OpCodes.Ldloc_2);
			il.Emit(OpCodes.Ldloc_1);
			MethodInfo info = typeof(Interlocked).GetMethods().Where(e => e.IsGenericMethod && e.Name == "CompareExchange").First();
			MethodInfo genericInfo = info.MakeGenericMethod(typeof(PropertyChangedEventHandler));
			il.Emit(OpCodes.Call, genericInfo);
			il.Emit(OpCodes.Stloc_0);
			il.Emit(OpCodes.Ldloc_0);
			il.Emit(OpCodes.Ldloc_1);
			il.Emit(OpCodes.Ceq);
			il.Emit(OpCodes.Ldc_I4_0);
			il.Emit(OpCodes.Ceq);
			il.Emit(OpCodes.Stloc_3);
			il.Emit(OpCodes.Ldloc_3);
			il.Emit(OpCodes.Brtrue_S, lb1);
			il.Emit(OpCodes.Ret);
		}

		private void DefineRemoveEventMethod(EventBuilder ev, FieldInfo propertyChangedField, string name) {
			MethodBuilder builder = TypeBuilder.DefineMethod(name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.NewSlot | MethodAttributes.HideBySig | MethodAttributes.Final,
				 null, new[] { typeof(PropertyChangedEventHandler) });
			builder.SetImplementationFlags(MethodImplAttributes.Managed);
			ILGenerator il = builder.GetILGenerator();
			ev.SetRemoveOnMethod(builder);

			MethodInfo miRemoveEvent = typeof(INotifyPropertyChanged).GetMethod("remove_PropertyChanged");
			TypeBuilder.DefineMethodOverride(builder, miRemoveEvent);
			il.DeclareLocal(typeof(PropertyChangedEventHandler));
			il.DeclareLocal(typeof(PropertyChangedEventHandler));
			il.DeclareLocal(typeof(PropertyChangedEventHandler));
			il.DeclareLocal(typeof(bool));

			Label lb1 = il.DefineLabel();
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldfld, propertyChangedField);
			il.Emit(OpCodes.Stloc_0);
			il.MarkLabel(lb1);
			il.Emit(OpCodes.Ldloc_0);
			il.Emit(OpCodes.Stloc_1);
			il.Emit(OpCodes.Ldloc_1);
			il.Emit(OpCodes.Ldarg_1);
			il.Emit(OpCodes.Call, typeof(Delegate).GetMethod("Remove", new Type[] { typeof(Delegate), typeof(Delegate) }));
			il.Emit(OpCodes.Castclass, typeof(PropertyChangedEventHandler));
			il.Emit(OpCodes.Stloc_2);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldflda, propertyChangedField);
			il.Emit(OpCodes.Ldloc_2);
			il.Emit(OpCodes.Ldloc_1);
			MethodInfo info = typeof(Interlocked).GetMethods().Where(e => e.IsGenericMethod && e.Name == "CompareExchange").First();
			MethodInfo genericInfo = info.MakeGenericMethod(typeof(PropertyChangedEventHandler));
			il.Emit(OpCodes.Call, genericInfo);
			il.Emit(OpCodes.Stloc_0);
			il.Emit(OpCodes.Ldloc_0);
			il.Emit(OpCodes.Ldloc_1);
			il.Emit(OpCodes.Ceq);
			il.Emit(OpCodes.Ldc_I4_0);
			il.Emit(OpCodes.Ceq);
			il.Emit(OpCodes.Stloc_3);
			il.Emit(OpCodes.Ldloc_3);
			il.Emit(OpCodes.Brtrue_S, lb1);
			il.Emit(OpCodes.Ret);
		}
	}
}
